package task.tests;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Properties;
import java.util.Scanner;
import task.AbstractAutomatonTask;
import task.config.AbstractAutomatonTaskConfig;
import util.StringUtils;
import util.Util;
import automaton.AutomatonMetaData;
import automaton.efsm.DefiniteEFSM;
import automaton.efsm.NondefiniteEFSM;

/**
 * Class implementing calculation of a fitness function of an automaton
 * based on tests.
 * @author Daniil Chivilikhin
 */
public class TestsTask extends AbstractAutomatonTask {
	private AutomatonTest[] tests;
	private ArrayList<String> events;
	private ArrayList[] actions;
	private String[] outputs;
	private AbstractAutomatonTaskConfig config;
	
	public TestsTask(AbstractAutomatonTaskConfig config) {
		this.config = config;
		configure(config.getProperty("tests"));
		outputs = new String[tests.length];
	}
	
	private void readTests(String filename) {
		Scanner in = null;
		try {
			in = new Scanner(new File(filename));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

		int numberOfTests = in.nextInt();
		tests = new AutomatonTest[numberOfTests];
		
		for (int i = 0; i < numberOfTests; i++) {
			String[] inputAndOutput = in.next().split("->");
			tests[i] = new AutomatonTest(inputAndOutput[0].split("_"), inputAndOutput[1].split("_"));
		}

	}
	
	private void configure(String propertiesFileName) {
		Properties config = new Properties();
		
		try {
			config.load(new FileInputStream(new File(propertiesFileName)));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		events = new ArrayList<String>();
		String eventArray[] = config.getProperty("events").split(" ");
		for (int i = 0; i < eventArray.length; i++) {
			events.add(eventArray[i]);
		}
		String[] act = config.getProperty("actions").split(" ");
		actions = new ArrayList[act.length];
		for (int i = 0; i < act.length; i++) {
			actions[i] = new ArrayList<String>();
			String[] tmp = act[i].split(",");
			for (int j = 0; j < tmp.length; j++) {
				if (tmp[j].equals("zn")) {
					actions[i].add("");
				} else {
					actions[i].add(tmp[j]);
				}
			}
		}
		
		readTests(config.getProperty("tests"));
	}

	private AutomatonMetaData runDFAOnTest(DefiniteEFSM dfa, int scenarioIndex) {
		int currentState = dfa.getInitialState();
		int numberOfSteps = 0;
		
		ArrayList<NondefiniteEFSM.Transition> visitedTransitions = new ArrayList<NondefiniteEFSM.Transition>();
		String answers = "";
		for (int i = 0; i < tests[scenarioIndex].input.length; i++) {
			int influenceIndex = events.indexOf(tests[scenarioIndex].getInput()[numberOfSteps++]);
			ArrayList<String> ans = dfa.transitions[currentState][influenceIndex].getActions();
			String answer = "";
			for (int j = 0; j < ans.size() - 1; j++) {
				answer += ans.get(j) + ",";
			}
			answer += ans.get(ans.size() - 1);
			int nextState = dfa.transitions[currentState][influenceIndex].getEndState();
			if (nextState == -1) {
				continue;
			}
			NondefiniteEFSM.Transition tr = new NondefiniteEFSM.Transition(currentState, nextState, ans, influenceIndex);
			if (!Util.listContains(visitedTransitions, tr)) {
				visitedTransitions.add(tr);
			}
			currentState = nextState;
			answers += answer;
		}
		outputs[scenarioIndex] = answers;
		double f = (double)tests[scenarioIndex].getLevensteinDistance(answers) / (double)outputs.length;
		return new AutomatonMetaData(dfa, visitedTransitions, f);
	}
	
	public String[] getAutomatonOutputs() {
		return outputs;
	}
	
	@Override
	public ArrayList[] getActions() {
		return actions;
	}
	
	@Override
	public ArrayList<String> getEvents() {
		return events;
	}

	@Override
	public AutomatonMetaData getFitness(DefiniteEFSM dfa) {
		ArrayList<NondefiniteEFSM.Transition> visitedTransitions = new ArrayList<NondefiniteEFSM.Transition>();
		double f1 = 0;
		for (int i = 0; i < tests.length; i++) {
			AutomatonMetaData dfaRunData = runDFAOnTest(dfa, i);
			f1 += 1.0 - dfaRunData.getFitness();
			for (NondefiniteEFSM.Transition tr : dfaRunData.getVisitedTransitions()) {
				if (!Util.listContains(visitedTransitions, tr)) {
					visitedTransitions.add(tr);
				}
			}
		}
		f1 /= (double)tests.length;
		
		double T = 100;
		
		int numberOfTransitions = visitedTransitions.size();
	
		if (f1 < 0) {
			return new AutomatonMetaData(dfa, visitedTransitions, 0.0);
		}
		
		if (f1 < 1.0) {
			return new AutomatonMetaData(dfa, visitedTransitions, 0.5 * T * f1 + 0.01 * (100 - numberOfTransitions));
		}
		return new AutomatonMetaData(dfa, visitedTransitions, T + 0.01 * (100 - numberOfTransitions));
	}
	
	/**
	 * Class representing a test scenario for an automaton.
	 * Stores the input and the output lists in the form 
	 * of string arrays.
	 */
	private class AutomatonTest {
		private String[] input;
		private String[] output;
		
		public AutomatonTest(String[] input, String[] output) {
			this.input = input.clone();
			this.output = output.clone();
		}
		
		public String[] getInput() {
			return input;
		}
		
		public int getLevensteinDistance(String answer) {
			String out = "";
			for (int i = 0; i < output.length; i++) {
				out += output[i];
			}
			return StringUtils.levensteinDistance(answer, out);
		}
	}
	
	public void printAutomataOutputs(String filename) {
		PrintWriter out = null;
		try {
			out = new PrintWriter(new File(filename));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		for (int i = 0; i < outputs.length; i++) {
			out.println(outputs[i]);
		}
		out.close();
	}
	
	@Override
	public int getDesiredNumberOfStates() {
		return config.desiredNumberOfStates();
	}
	@Override
	public double getDesiredFitness() {
		return config.desiredFitness();
	}
	@Override
	public double getMinimumFitness() {
		return config.minimumFitness();
	}
}
